package com.hfi.util;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author ChangLiang
 * @date 2019/6/4
 */
public class Hashids {
    private static final String DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";

    private String salt = "";
    private String alphabet = "";
    private String seps = "cfhistuCFHISTU";
    private int minHashLength = 0;
    private String guards;

    public Hashids() {
        this("");
    }

    public Hashids(String salt) {
        this(salt, 0);
    }

    public Hashids(String salt, int minHashLength) {
        this(salt, minHashLength, DEFAULT_ALPHABET);
    }

    public Hashids(String salt, int minHashLength, String alphabet) {
        this.salt = salt;
        if(minHashLength < 0) {
            this.minHashLength = 0;
        }
        else {
            this.minHashLength = minHashLength;
        }
        this.alphabet = alphabet;

        String uniqueAlphabet = "";
        for(int i = 0; i < this.alphabet.length(); i++){
            if(!uniqueAlphabet.contains("" + this.alphabet.charAt(i))){
                uniqueAlphabet += "" + this.alphabet.charAt(i);
            }
        }

        this.alphabet = uniqueAlphabet;

        int minAlphabetLength = 16;
        if(this.alphabet.length() < minAlphabetLength){
            throw new IllegalArgumentException("alphabet must contain at least " + minAlphabetLength + " unique characters");
        }

        if(this.alphabet.contains(" ")){
            throw new IllegalArgumentException("alphabet cannot contains spaces");
        }

        // seps should contain only characters present in alphabet;
        // alphabet should not contains seps
        for(int i = 0; i < this.seps.length(); i++){
            int j = this.alphabet.indexOf(this.seps.charAt(i));
            if(j == -1){
                this.seps = this.seps.substring(0, i) + " " + this.seps.substring(i + 1);
            } else {
                this.alphabet = this.alphabet.substring(0, j) + " " + this.alphabet.substring(j + 1);
            }
        }

        this.alphabet = this.alphabet.replaceAll("\\s+", "");
        this.seps = this.seps.replaceAll("\\s+", "");
        this.seps = this.consistentShuffle(this.seps, this.salt);

        double sepDiv = 3.5;
        if((this.seps.equals("")) || ((this.alphabet.length() / this.seps.length()) > sepDiv)){
            int seps_len = (int)Math.ceil(this.alphabet.length() / sepDiv);

            if(seps_len == 1){
                seps_len++;
            }

            if(seps_len > this.seps.length()){
                int diff = seps_len - this.seps.length();
                this.seps += this.alphabet.substring(0, diff);
                this.alphabet = this.alphabet.substring(diff);
            } else {
                this.seps = this.seps.substring(0, seps_len);
            }
        }

        this.alphabet = this.consistentShuffle(this.alphabet, this.salt);
        // use double to round up
        int guardDiv = 12;
        int guardCount = (int)Math.ceil((double)this.alphabet.length() / guardDiv);

        if(this.alphabet.length() < 3){
            this.guards = this.seps.substring(0, guardCount);
            this.seps = this.seps.substring(guardCount);
        } else {
            this.guards = this.alphabet.substring(0, guardCount);
            this.alphabet = this.alphabet.substring(guardCount);
        }
    }

    /**
     * Encrypt numbers to string
     *
     * @param numbers the numbers to encrypt
     * @return the encrypt string
     */
    public String encode(long... numbers){
        for (long number : numbers) {
            if (number > 9007199254740992L) {
                throw new IllegalArgumentException("number can not be greater than 9007199254740992L");
            }
        }

        String retval = "";
        if(numbers.length == 0) {
            return retval;
        }

        return this._encode(numbers);
    }

    /**
     * Decrypt string to numbers
     *
     * @param hash the encrypt string
     * @return decryped numbers
     */
    public long[] decode(String hash){
        long[] ret = {};

        if(hash.equals("")) {
            return ret;
        }

        return this._decode(hash, this.alphabet);
    }

    /**
     * Encrypt hexa to string
     *
     * @param hexa the hexa to encrypt
     * @return the encrypt string
     */
    public String encodeHex(String hexa){
        if(!hexa.matches("^[0-9a-fA-F]+$")) {
            return "";
        }

        List<Long> matched = new ArrayList<Long>();
        Matcher matcher = Pattern.compile("[\\w\\W]{1,12}").matcher(hexa);

        while (matcher.find()) {
            matched.add(Long.parseLong("1" + matcher.group(), 16));
        }

        // conversion
        long[] result = new long[matched.size()];
        for(int i = 0; i < matched.size(); i++) {
            result[i] = matched.get(i);
        }

        return this._encode(result);
    }

    /**
     * Decrypt string to numbers
     *
     * @param hash the encrypt string
     * @return decryped numbers
     */
    public String decodeHex(String hash){
        String result = "";
        long[] numbers = this.decode(hash);

        for (long number : numbers) {
            result += Long.toHexString(number).substring(1);
        }

        return result;
    }

    private String _encode(long... numbers){
        int numberHashInt = 0;
        for(int i = 0; i < numbers.length; i++){
            numberHashInt += (numbers[i] % (i+100));
        }
        String alphabet = this.alphabet;
        char ret = alphabet.toCharArray()[numberHashInt % alphabet.length()];
        //char lottery = ret;
        long num;
        int sepsIndex, guardIndex;
        String buffer, ret_str = ret + "";
        char guard;

        for(int i = 0; i < numbers.length; i++){
            num = numbers[i];
            buffer = ret + this.salt + alphabet;

            alphabet = this.consistentShuffle(alphabet, buffer.substring(0, alphabet.length()));
            String last = this.hash(num, alphabet);

            ret_str += last;

            if(i + 1 < numbers.length){
                num %= ((int)last.toCharArray()[0] + i);
                sepsIndex = (int)(num % this.seps.length());
                ret_str += this.seps.toCharArray()[sepsIndex];
            }
        }

        if(ret_str.length() < this.minHashLength){
            guardIndex = (numberHashInt + (int)(ret_str.toCharArray()[0])) % this.guards.length();
            guard = this.guards.toCharArray()[guardIndex];

            ret_str = guard + ret_str;

            if(ret_str.length() < this.minHashLength){
                guardIndex = (numberHashInt + (int)(ret_str.toCharArray()[2])) % this.guards.length();
                guard = this.guards.toCharArray()[guardIndex];

                ret_str += guard;
            }
        }

        int halfLen = alphabet.length() / 2;
        while(ret_str.length() < this.minHashLength){
            alphabet = this.consistentShuffle(alphabet, alphabet);
            ret_str = alphabet.substring(halfLen) + ret_str + alphabet.substring(0, halfLen);
            int excess = ret_str.length() - this.minHashLength;
            if(excess > 0){
                int start_pos = excess / 2;
                ret_str = ret_str.substring(start_pos, start_pos + this.minHashLength);
            }
        }

        return ret_str;
    }

    private long[] _decode(String hash, String alphabet){
        ArrayList<Long> ret = new ArrayList<Long>();

        int i = 0;
        String regexp = "[" + this.guards + "]";
        String hashBreakdown = hash.replaceAll(regexp, " ");
        String[] hashArray = hashBreakdown.split(" ");

        if(hashArray.length == 3 || hashArray.length == 2){
            i = 1;
        }

        hashBreakdown = hashArray[i];

        char lottery = hashBreakdown.toCharArray()[0];

        hashBreakdown = hashBreakdown.substring(1);
        hashBreakdown = hashBreakdown.replaceAll("[" + this.seps + "]", " ");
        hashArray = hashBreakdown.split(" ");

        String subHash, buffer;
        for (String aHashArray : hashArray) {
            subHash = aHashArray;
            buffer = lottery + this.salt + alphabet;
            alphabet = this.consistentShuffle(alphabet, buffer.substring(0, alphabet.length()));
            ret.add(this.unhash(subHash, alphabet));
        }

        //transform from List<Long> to long[]
        long[] arr = new long[ret.size()];
        for(int k = 0; k < arr.length; k++){
            arr[k] = ret.get(k);
        }

        if(!this._encode(arr).equals(hash)){
            arr = new long[0];
        }

        return arr;
    }

    /* Private methods */
    private String consistentShuffle(String alphabet, String salt){
        if(salt.length() <= 0) {
            return alphabet;
        }

        char[] arr = salt.toCharArray();
        int asc_val, j;
        char tmp;
        for(int i = alphabet.length() - 1, v = 0, p = 0; i > 0; i--, v++){
            v %= salt.length();
            asc_val = (int)arr[v];
            p += asc_val;
            j = (asc_val + v + p) % i;

            tmp = alphabet.charAt(j);
            alphabet = alphabet.substring(0, j) + alphabet.charAt(i) + alphabet.substring(j + 1);
            alphabet = alphabet.substring(0, i) + tmp + alphabet.substring(i + 1);
        }

        return alphabet;
    }

    private String hash(long input, String alphabet){
        String hash = "";
        int alphabetLen = alphabet.length();
        char[] arr = alphabet.toCharArray();

        do {
            hash = arr[(int)(input % alphabetLen)] + hash;
            input /= alphabetLen;
        } while(input > 0);

        return hash;
    }

    private Long unhash(String input, String alphabet){
        long number = 0, pos;
        char[] input_arr = input.toCharArray();

        for(int i = 0; i < input.length(); i++){
            pos = alphabet.indexOf(input_arr[i]);
            number += pos * Math.pow(alphabet.length(), input.length() - i - 1);
        }

        return number;
    }

    @SuppressWarnings("unused")
    public static int checkedCast(long value) {
        int result = (int) value;
        if (result != value) {
            // don't use checkArgument here, to avoid boxing
            throw new IllegalArgumentException("Out of range: " + value);
        }
        return result;
    }

    /**
     * Get version
     *
     * @return version
     */
    @SuppressWarnings("unused")
    public String getVersion() {
        return "1.0.0";
    }
}
